unittest.mock

您所在的位置:网站首页 unitytest mock unittest.mock

unittest.mock

2024-07-15 00:21| 来源: 网络整理| 查看: 265

自动 spec¶

自动 spec 是基于现有 mock 的 spec 特性。 它将 mock 的 api 限制为原始对象 (spec) 的 api,但它是递归(惰性实现)的因而 mock 的属性只有与 spec 的属性相同的 api。 除此之外被模块的函数 / 方法具有与原对应物相同的调用签名因此如果它们被不正确地调用时会引发 TypeError。

在我开始解释自动 spec 如何运作之前,先说明一下它的必要性。

Mock 是个非常强大和灵活的对象,但对 mock 操作来说有一个普遍存在的缺陷。 如果你重构了你的部分代码,如修改成员的名称等,则针对仍然使用 旧 API 但其使用的是 mock 而非真实对象的代码的测试仍将通过。 这意味着即使你的代码已被破坏你的测试却仍可能全部通过。

在 3.5 版本发生变更: 在 3.5 之前,在词断言中带有拼写错误应当引发错误的测试将会静默地通过。 你仍然可通过向 Mock 传入 unsafe=True 来实现此行为。

请注意这是为什么你在单元测试之外还需要集成测试的原因之一。 孤立地测试每个部分时全都正常而顺滑,但是如果你没有测试你的各个单元“联成一体”的情况如何那么就仍然存在测试可以发现大量错误的空间。

mock 已经提供了一个对此有帮助的特性,称为 spec 控制。 如果你使用类或实例作为一个 mock 的 spec 那么你将仅能访问 mock 中只存在于实际的类中的属性。

>>> from urllib import request >>> mock = Mock(spec=request.Request) >>> mock.assret_called_with # Intentional typo! Traceback (most recent call last): ... AttributeError: Mock object has no attribute 'assret_called_with'

这个 spec 仅会应用于 mock 本身,因此对于 mock 中的任意方法我们仍然面临相同的问题:

>>> mock.has_data() >>> mock.has_data.assret_called_with() # Intentional typo!

自动 spec 解决了这个问题。 你可以将 autospec=True 传给 patch() / patch.object() 或是使用 create_autospec() 函数来创建带有 spec 的 mock。 如果你是将 autospec=True 参数传给 patch() 那么被替代的那个对象将被用作 spec 对象。 因为 spec 控制是“惰性地”执行的(spec 在 mock 中的属性被访问时才会被创建)所以即使是非常复杂或深度嵌套的对象(例如需要导入本身已导入了多个模块的模块)你也可以使用它而不会有太大的性能损失。

以下是一个实际应用的示例:

>>> from urllib import request >>> patcher = patch('__main__.request', autospec=True) >>> mock_request = patcher.start() >>> request is mock_request True >>> mock_request.Request

你可以看到 request.Request 有一个 spec。 request.Request 的构造器接受两个参数 (其中一个是 self)。 如果我们尝试不正确地调用它则将导致:

>>> req = request.Request() Traceback (most recent call last): ... TypeError: () takes at least 2 arguments (1 given)

该 spec 也将应用于被实例化的类(即附带i.e. the return value of spec 的 mock 的返回值):

>>> req = request.Request('foo') >>> req

Request 对象是不可调用对象,因此实例化我们的被模拟 request.Request 的返回值是一个不可调用的 mock。 有了这个 spec 我们的断言中出现任何拼写问题都将引发正确的错误:

>>> req.add_header('spam', 'eggs') >>> req.add_header.assret_called_with # Intentional typo! Traceback (most recent call last): ... AttributeError: Mock object has no attribute 'assret_called_with' >>> req.add_header.assert_called_with('spam', 'eggs')

在许多情况下你将只需将 autospec=True 添加到你现有的 patch() 调用中即可防止拼写错误和 api 变化所导致的问题。

除了通过 patch() 来使用 autospec 还有一个 create_autospec() 可以直接创建带有自动 spec 的 mock:

>>> from urllib import request >>> mock_request = create_autospec(request) >>> mock_request.Request('foo', 'bar')

不过这并非没有缺点和限制,这也就是为什么它不是默认行为。 为了知道在 spec 对象上有哪些属性是可用的,autospec 必须对 spec 进行自省(访问其属性)。 当你遍历 mock 上的属性时在原始对象上的对应遍历也将在底层进行。 如果你的任何带 spec 的对象具有可触发代码执行的特征属性或描述器则你可能会无法使用 autospec。 在另一方面更好的的做法是将你的对象设计为可以安全地执行自省 [4]。

一个更严重的问题在于实例属性通常是在 __init__() 方法中被创建而在类中完全不存在。 autospec 无法获取动态创建的属性而使得 api 被限制于可见的属性。

>>> class Something: ... def __init__(self): ... self.a = 33 ... >>> with patch('__main__.Something', autospec=True): ... thing = Something() ... thing.a ... Traceback (most recent call last): ... AttributeError: Mock object has no attribute 'a'

要解决这个问题有几种不同的方式。 最容易但多少有些烦扰的方式是简单地在 mock 创建完成后再设置所需的属性。 Just because autospec 只是不允许你获取不存在于 spec 上的属性但并不会阻止你设置它们:

>>> with patch('__main__.Something', autospec=True): ... thing = Something() ... thing.a = 33 ...

spec 和 autospec 都有更严格的版本 确实能 阻止你设置不存在的属性。 这在你希望确保你的代码只能 设置 有效的属性时也很有用,但显然它会阻止下面这个特定的应用场景:

>>> with patch('__main__.Something', autospec=True, spec_set=True): ... thing = Something() ... thing.a = 33 ... Traceback (most recent call last): ... AttributeError: Mock object has no attribute 'a'

解决此问题的最好方式可能是添加类属性作为在 __init__() 中初始化的实例属性的默认值。 请注意如果你只在. Note that if you are only setting default attributes in __init__() 中设置默认属性那么通过类属性来提供它们(当然会在实例之间共享)也将有更快的速度。 例如

class Something: a = 33

这带来了另一个问题。 为今后将变为不同类型对象的那些成员提供默认值 None 是比较常见的做法。 None 作为 spec 是没有用处的,因为它会使你无法访问 any 任何属性或方法。 由于 None 作为 spec 将 永远不会 有任何用处,并且有可能要指定某个通常为其他类型的成员,因此 autospec 不会为被设为 None 的成员使用 spec。 它们将为普通的 mock (嗯 —— 应为 MagicMocks):

>>> class Something: ... member = None ... >>> mock = create_autospec(Something) >>> mock.member.foo.bar.baz()

如果你不喜欢修改你的生产类来添加默认值那么还有其他的选项。 其中之一是简单地使用一个实例而非类作为 spec。 另一选项则是创建一个生产类的子类并向该子类添加默认值而不影响到生产类。 这两个选项都需要你使用一个替代对象作为 spec。 值得庆幸的是 patch() 支持这样做 —— 你可以简单地传入替代对象作为 autospec 参数:

>>> class Something: ... def __init__(self): ... self.a = 33 ... >>> class SomethingForTest(Something): ... a = 33 ... >>> p = patch('__main__.Something', autospec=SomethingForTest) >>> mock = p.start() >>> mock.a [4]

这仅适用于类或已实例化的对象。 调用一个被模拟的类来创建一个 mock 实例 不会 创建真的实例。 只有属性查找 —— 以及对 dir() 的调用 —— 会被执行。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3